import time
from itertools import takewhile
import operator
from collections import OrderedDict
from abc import abstractmethod, ABC
import numpy as np
import imagehash


class IStorage(ABC):
    """
    Local storage for this node.
    IStorage implementations of get must return the same type as put in by set
    """

    @abstractmethod
    def __setitem__(self, key, value):
        """
        Set a key to the given value.
        """

    @abstractmethod
    def __getitem__(self, key):
        """
        Get the given key.  If item doesn't exist, raises C{KeyError}
        """

    @abstractmethod
    def get(self, key, default=None):
        """
        Get given key.  If not found, return default.
        """

    @abstractmethod
    def iter_older_than(self, seconds_old):
        """
        Return the an iterator over (key, value) tuples for items older
        than the given secondsOld.
        """

    @abstractmethod
    def __iter__(self):
        """
        Get the iterator for this storage, should yield tuple of (key, value)
        """


class ForgetfulStorage(IStorage):
    def __init__(self, ttl=604800):
        """
        By default, max age is a week.
        """
        self.data = OrderedDict()
        self.ttl = ttl

    def __setitem__(self, key, value):
        if key in self.data:
            del self.data[key]
        self.data[key] = (time.monotonic(), value)
        self.cull()

    def cull(self):
        for _, _ in self.iter_older_than(self.ttl):
            self.data.popitem(last=False)

    def get(self, key, default=None):
        self.cull()
        if key in self.data:
            return self[key]
        return default

    def __getitem__(self, key):
        self.cull()
        return self.data[key][1]

    def __repr__(self):
        self.cull()
        return repr(self.data)

    def iter_older_than(self, seconds_old):
        min_birthday = time.monotonic() - seconds_old
        zipped = self._triple_iter()
        matches = takewhile(lambda r: min_birthday >= r[1], zipped)
        return list(map(operator.itemgetter(0, 2), matches))

    def _triple_iter(self):
        ikeys = self.data.keys()
        ibirthday = map(operator.itemgetter(0), self.data.values())
        ivalues = map(operator.itemgetter(1), self.data.values())
        return zip(ikeys, ibirthday, ivalues)

    def __iter__(self):
        self.cull()
        ikeys = self.data.keys()
        ivalues = map(operator.itemgetter(1), self.data.values())
        return zip(ikeys, ivalues)


class PHashStorage(IStorage):
    def __init__(self):
        """
        By default, max age is a week.
        """
        self.data = OrderedDict()
        self.phash_matrix = np.zeros(shape=(0, 64), dtype=bool)
        self.set("DEFAULT", "0000000000000000")
        # 8ba195d39892f559
        # 0000000000000000
        
    def __setitem__(self, key, value):
        self.data[key] = value

    def set(self, key, value):
        if not self.data.get(key):
            self.data[key] = value
            phash = imagehash.hex_to_hash(value)
            self.phash_matrix = np.append(
                self.phash_matrix, [phash.hash.flatten()], axis=0
            )

    def get(self, key, default=None):
        if key in self.data:
            return self[key]
        return default

    # @nb.jit(nopython=True)
    # https://deepinout.com/numpy/numpy-questions/178_numpy_fast_hamming_distance_computation_between_binary_numpy_arrays.html
    def hamming_distances(self, val):
        
        if len(self.data) == 0:
            return -1, 255
        try:
            diff = np.bitwise_xor(val, self.phash_matrix)
            hds = np.count_nonzero(diff, axis=1)
            return list(self.data.keys())[hds.argmin()], int(hds.min())
        except Exception as e:
            print(
                "hamming_distances",
                len(list(self.data.keys())),
                len(self.data),
                hds.argmin(),
                int(hds.min()),
                len(hds),
                len(diff),
                len(self.phash_matrix),
            )

    def __getitem__(self, key):
        return self.data[key]

    def __repr__(self):
        return repr(self.data)

    def __len__(self):
        return len(self.data)

    def iter_older_than(self, seconds_old):
        # min_birthday = time.monotonic() - seconds_old
        # zipped = self._triple_iter()
        # matches = takewhile(lambda r: min_birthday >= r[1], zipped)
        # return list(map(operator.itemgetter(0, 2), matches))
        pass

    # def _triple_iter(self):
    #     ikeys = self.data.keys()
    #     ibirthday = map(operator.itemgetter(0), self.data.values())
    #     ivalues = map(operator.itemgetter(1), self.data.values())
    #     return zip(ikeys, ibirthday, ivalues)

    def __iter__(self):
        # self.cull()
        ikeys = self.data.keys()
        ivalues = map(operator.itemgetter(1), self.data.values())
        return zip(ikeys, ivalues)
        pass
